#! perl -slw
use strict;
use POSIX;
use XML::Writer;
use IO::File;

#initialise XML file writer
my $output_filename = "output.xml";
my $output = new IO::File(">$output_filename");
my $writer = new XML::Writer(OUTPUT => $output);

#initialise variables
my $message_count;
my $var;

#my $nbtroot_ref_offset;
my $depth; my $depth_bbt; my $depth_root;

sub readbin {
	seek(FH,$_[0],0);
	read(FH,$var,$_[1]);
	my $out = unpack($_[2],$var);
	return $out;
}

#different subroutines for different depths
sub nbt_depth0 {
	my $nbtroot_ref_offset = $_[0]; # $_[0] is the starting offset
	my $nbt_child_sizeof_btentry = $_[2];
	
	for (my $i=1; $i<=$_[1]; $i++) { # $_[1] is the number of child entries
		$writer->startTag("child", "num" => $i, "depth" => $_[3]);
		my $nbt_nid_0depth = readbin($nbtroot_ref_offset,8,'Q*'); #get nid

		#check nid type
		my $temp = unpack('b*', $var);
		my $nbt_nidtype_0depth = substr($temp, 0, 5); #check nid type
		if($nbt_nidtype_0depth == '00100') { $message_count++; $nbt_nidtype_0depth = "message";	} #count messages
		#elsif($nbt_nidtype_0depth == '10100') { $attachment_count++; $nbt_nidtype_0depth = "attachment";	} #count messages
		else { $nbt_nidtype_0depth = "no-type"; }

		my $nbt_bid_0depth = readbin($nbtroot_ref_offset + 8,8,'Q*'); #get bid of data block
		my $nbt_bid_sub_0depth = readbin($nbtroot_ref_offset + 16,8,'Q*'); #get bid sub data-block
		if($nbt_bid_sub_0depth == 0 ) { $nbt_bid_sub_0depth = ""; } #so it displays empty element in XML
		my $nbt_parent_nid_0depth = readbin($nbtroot_ref_offset + 24,4,'I*'); #get parent nid
		if($nbt_parent_nid_0depth == 0 ) { $nbt_parent_nid_0depth = ""; } #so it displays empty element in XML

		$writer->startTag("nid"); $writer->characters($nbt_nid_0depth); $writer->endTag("nid");
		$writer->startTag("parent-nid"); $writer->characters($nbt_parent_nid_0depth); $writer->endTag("parent-nid");
		$writer->startTag("bid"); $writer->characters($nbt_bid_0depth); $writer->endTag("bid");
		$writer->startTag("sub-bid"); $writer->characters($nbt_bid_sub_0depth); $writer->endTag("sub-bid");
		$writer->startTag("type"); $writer->characters($nbt_nidtype_0depth); $writer->endTag("type");

		# go through sub-blocks if they exist!! TO DO

		$writer->endTag("child");
		$nbtroot_ref_offset = $nbtroot_ref_offset + $nbt_child_sizeof_btentry;
	}
}

sub nbt_depthover0 {
	my $depth1_nbtroot_ref_offset = $_[0];
	my $nbt_child_num_entries = $_[1];
	my $nbt_child_sizeof_btentry = $_[2];
	my $depth_saved = $_[3];

	for (my $j=1; $j<=$nbt_child_num_entries; $j++) {
		$writer->startTag("child", "num" => $j, "depth" => $depth_saved);
		
		seek( FH, $depth1_nbtroot_ref_offset + 8, 0 ); #get block ID (BID)
		read( FH, $var, 8 );
		my $depth1_nbtroot_btentry_bid = unpack('b*', $var); #unpack to binary string
		my $depth1_nbtroot_btentry_bid_i = substr($depth1_nbtroot_btentry_bid, 1, 1); #check if it is an internal block
		if($depth1_nbtroot_btentry_bid_i == 1) { $depth1_nbtroot_btentry_bid_i= "yes" } else { $depth1_nbtroot_btentry_bid_i = "no" }
		$depth1_nbtroot_btentry_bid = substr($depth1_nbtroot_btentry_bid, 2, 62); 
		$depth1_nbtroot_btentry_bid = unpack('Q*', $var); #restore string back to HEX
		
		my $depth1_nbtroot_btentrykey = readbin($depth1_nbtroot_ref_offset,8,'Q*'); #find BTKey
		my $depth1_nbtroot_child_ref_offset = readbin($depth1_nbtroot_ref_offset + 16,8,'Q*'); #find offset	
		my $depth1_nbt_child_num_entries = readbin($depth1_nbtroot_child_ref_offset + 488,1,'C*'); #find number of entries
		my $depth1_nbt_child_bid = readbin($depth1_nbtroot_ref_offset + 504,8,'Q*'); # get bid of page file
		my $depth1_nbt_child_sizeof_btentry = readbin($depth1_nbtroot_child_ref_offset + 490,8,'C*'); # get size of entries
		my $depth1_depth = readbin($depth1_nbtroot_child_ref_offset + 491,8,'C*'); #check the depth

		$writer->startTag("details");
		$writer->startTag("nid"); $writer->characters($depth1_nbtroot_btentrykey); $writer->endTag("nid");
		$writer->startTag("bid"); $writer->characters($depth1_nbtroot_btentry_bid); $writer->endTag("bid");
		$writer->startTag("block-offset"); $writer->characters($depth1_nbtroot_child_ref_offset); $writer->endTag("block-offset");
		$writer->startTag("num-of-entries"); $writer->characters($depth1_nbt_child_num_entries); $writer->endTag("num-of-entries");
		$writer->startTag("internal"); $writer->characters($depth1_nbtroot_btentry_bid_i); $writer->endTag("internal");
		$writer->endTag("details");

		#now lets go through child elements
		if ($depth1_depth > 0) {
			nbt_depthover0($depth1_nbtroot_child_ref_offset,$depth1_nbt_child_num_entries,
			$depth1_nbt_child_sizeof_btentry,$depth1_depth);
		}
		else {
			nbt_depth0($depth1_nbtroot_child_ref_offset,$depth1_nbt_child_num_entries,
			$depth1_nbt_child_sizeof_btentry,$depth1_depth);
		}

		$writer->endTag("child");
		$depth1_nbtroot_ref_offset = $depth1_nbtroot_ref_offset + $nbt_child_sizeof_btentry;
	}#end for
}

sub bbt_depth0 {
	my $bbtroot_ref_offset = $_[0]; # $_[0] is the starting offset
	my $bbt_child_sizeof_btentry = $_[2];
	
	for (my $i=1; $i<=$_[1]; $i++) { # $_[1] is the number of child entries
		$writer->startTag("child", "id" => $i, "depth" => $_[3]);
	
		#get block ID
		seek( FH, $bbtroot_ref_offset, 0 );
		read( FH, $var, 8 );
		my $bbt_child_child_bid = unpack('b*', $var);
		my $bbt_child_child_bid_i = substr($bbt_child_child_bid, 1, 1); # check if it is an internal block
		#if($bbt_child_child_bid_i == 1) { $bbt_child_child_bid_i= "yes" } else { $bbt_child_child_bid_i = "" }
		$bbt_child_child_bid = substr($bbt_child_child_bid, 2, 62);
		$bbt_child_child_bid = unpack('Q*', $var);

		my $bbt_child_child_ib = readbin($bbtroot_ref_offset + 8,8,'Q*'); #get ib offset
		my $bbt_child_child_cb = readbin($bbtroot_ref_offset + 16,2,'S*'); #get size of data in block, in bytes
		my $bbt_child_child_cref = readbin($bbtroot_ref_offset + 18,2,'S*'); # number of times a block is referenced
		
		#get actual size of block
		my $multiple_64 = $bbt_child_child_cb + 16; #16 bytes needs to be added for the blocktrailer
		$multiple_64 = $multiple_64 / 64; $multiple_64 = ceil($multiple_64); $multiple_64 = $multiple_64 * 64;

		#save in XML
		$writer->startTag("bid"); $writer->characters($bbt_child_child_bid); $writer->endTag("bid");
		$writer->startTag("offset"); $writer->characters($bbt_child_child_ib); $writer->endTag("offset");
		$writer->startTag("block-size"); $writer->characters($multiple_64); $writer->endTag("block-size");
		$writer->startTag("data-size"); $writer->characters($bbt_child_child_cb); $writer->endTag("data-size");
		$writer->startTag("referenced"); $writer->characters($bbt_child_child_cref); $writer->endTag("referenced");
		
		if($bbt_child_child_bid_i == 1) {
			$writer->startTag("internal"); 	
			
			#we need to check whether it is a SL/SI block
			my $subnode_type = readbin($bbt_child_child_ib,1,'C*'); #find type
			my $subnode_depth = readbin($bbt_child_child_ib + 1,1,'C*'); #find depth

			if ($subnode_type == 2 and $subnode_depth == 0) { #SL-entry	
				$writer->startTag("sl-entry");
				my $number_of_entries_in_block = readbin($bbt_child_child_ib + 2,2,'C*'); #find number of entries

				my $temp_entries_start = $bbt_child_child_ib + 8;
				for (my $h=1; $h<= $number_of_entries_in_block; $h++) {	

					my $slentry_nid = readbin($temp_entries_start,4,'L*'); #local nid
					my $slentry_bid = readbin($temp_entries_start + 8,8,'Q*');
					my $slentry_sub_bid = readbin($temp_entries_start + 16,8,'Q*');
					
					$writer->startTag("local-nid"); $writer->characters($slentry_nid); $writer->endTag("local-nid");
					$writer->startTag("bid"); $writer->characters($slentry_bid); $writer->endTag("bid");
					$writer->startTag("sub-bid"); $writer->characters($slentry_sub_bid); $writer->endTag("sub-bid");

					$temp_entries_start = $temp_entries_start + 24;
				} #end of for loop
				$writer->endTag("sl-entry");
			} #end of if sl-block

			if ($subnode_type == 1 and $subnode_depth == 1) { #check for xblocks
				$writer->startTag("xblock-entry");
				my $number_of_entries_in_xblock = readbin($bbt_child_child_ib + 2,2,'C*'); #find number of entries in xblock

				my $temp_entries_start_xblock = $bbt_child_child_ib + 8;
				for (my $h=1; $h<=$number_of_entries_in_xblock; $h++) {	
				
					my $xblock_bid = readbin($temp_entries_start_xblock,8,'Q*'); # BID
					$writer->startTag("bid"); $writer->characters($xblock_bid); $writer->endTag("bid");
					$temp_entries_start_xblock = $temp_entries_start_xblock + 8;
					
				} #end of for loop
				$writer->endTag("xblock-entry");
			} #end of if xblocks
			
			$writer->endTag("internal");
		} #end of if internal block
		
		$writer->endTag("child");		
		$bbtroot_ref_offset = $bbtroot_ref_offset + $bbt_child_sizeof_btentry;
		
	} #end of for loop
}

sub bbt_depthover0 {
	my $depth1_bbtroot_ref_offset = $_[0];
	my $num_entries = $_[1];
	my $bbt_child_sizeof = $_[2];
	my $depth_saved = $_[3];
	
	for (my $j=1; $j<=$num_entries; $j++) {
		$writer->startTag("child", "num" => $j, "depth" => $depth_saved);

		seek( FH, $depth1_bbtroot_ref_offset + 8, 0 ); #get block ID (BID)
		read( FH, $var, 8 );
		my $bbtroot_btentry_bid = unpack('b*', $var); #unpack to binary string
		my $bbtroot_btentry_bid_i = substr($bbtroot_btentry_bid, 1, 1); #check if it is an internal block
		if($bbtroot_btentry_bid_i == 1) { $bbtroot_btentry_bid_i= "yes" } else { $bbtroot_btentry_bid_i = "no" }
		$bbtroot_btentry_bid = substr($bbtroot_btentry_bid, 2, 62); 
		$bbtroot_btentry_bid = unpack('Q*', $var); #restore string back to HEX
		
		my $bbtroot_btentrykey = readbin($depth1_bbtroot_ref_offset,8,'Q*'); #find BTKey
		my $bbtroot_ref_offset_child = readbin($depth1_bbtroot_ref_offset + 16,8,'Q*'); #find offset
		my $bbt_child_num_entries = readbin($bbtroot_ref_offset_child + 488,1,'C*'); #find number of entries
		my $bbt_child_bid = readbin($depth1_bbtroot_ref_offset + 504,8,'Q*'); #get bid of page file
		my $bbt_child_sizeof_btentry = readbin($bbtroot_ref_offset_child + 490,1,'C*'); #get size of entries
		$depth_bbt = readbin($bbtroot_ref_offset_child + 491,1,'C*'); #check the depth

		#save details in XML
		$writer->startTag("details");
			$writer->startTag("nid"); $writer->characters($bbtroot_btentrykey); $writer->endTag("nid");
			$writer->startTag("bid"); $writer->characters($bbtroot_btentry_bid); $writer->endTag("bid");
			$writer->startTag("block-offset"); $writer->characters($bbtroot_ref_offset_child); $writer->endTag("block-offset");
			$writer->startTag("num-of-entries"); $writer->characters($bbt_child_num_entries); $writer->endTag("num-of-entries");
		$writer->endTag("details");	

		#now lets go through child elements
		if ($depth_bbt > $depth_root ) { die "something has gone wrong!\n"; } #bit of error catching here
		elsif ($depth_bbt > 0) {
			bbt_depthover0($bbtroot_ref_offset_child,$bbt_child_num_entries,$bbt_child_sizeof_btentry,$depth_bbt);
		}
		else {
			bbt_depth0($bbtroot_ref_offset_child,$bbt_child_num_entries,$bbt_child_sizeof_btentry,$depth_bbt);
		}	
		
		$writer->endTag("child");
		$depth1_bbtroot_ref_offset = $depth1_bbtroot_ref_offset + $bbt_child_sizeof;
	} #end for loop
} #end sub

#lets start the program
open(FH,'<',$ARGV[0]) or die "Can't open file $ARGV[0]\n";
binmode(FH); # binary

my $pst_ver = readbin(10,2,'C*'); # check file version
if ($pst_ver != 23) {die "This script will only work for Unicode versions\n";}

### START OF NBT PARSING ###
my $nbtroot = readbin(224,8,'Q*'); #NBT root page offset
my $nbtroot_bid = readbin($nbtroot + 224,8,'Q*'); #NBT root page BID

#get data from the NBT root page
my $nbtroot_num_entries = readbin($nbtroot + 488,1,'C*'); #get number of entries in page file
my $nbtroot_sizeof_btentry = readbin($nbtroot + 490,1,'C*'); #get size of entries
$depth = readbin($nbtroot + 491,1,'C*'); #check the depth of the page

#save this data in XML
$writer->startTag("root");
$writer->startTag("nbt");
$writer->startTag("nbt-root-details");
	$writer->startTag("bid"); $writer->characters($nbtroot_bid); $writer->endTag("bid");
	$writer->startTag("block-offset"); $writer->characters($nbtroot); $writer->endTag("block-offset");
	$writer->startTag("num-of-entries"); $writer->characters($nbtroot_num_entries); $writer->endTag("num-of-entries");
$writer->endTag("nbt-root-details");

my $offset = $nbtroot; #we need to use the orginal nbtroot later
printf "Parsing through the node BTREE...\n";

#lets start parsing through the first level nodes
for (my $i=1; $i<=$nbtroot_num_entries; $i++) {
	$writer->startTag("child", "num" => $i, "depth" => $depth);

	seek( FH, $offset + 8, 0 ); #get block ID (BID)
	read( FH, $var, 8 );
	my $nbtroot_btentry_bid = unpack('b*', $var); #unpack to binary string
	my $nbtroot_btentry_bid_i = substr($nbtroot_btentry_bid, 1, 1); #check if it is an internal block
	if($nbtroot_btentry_bid_i == 1) { $nbtroot_btentry_bid_i= "yes" } else { $nbtroot_btentry_bid_i = "no" }
	$nbtroot_btentry_bid = substr($nbtroot_btentry_bid, 2, 62); 
	$nbtroot_btentry_bid = unpack('Q*', $var); #restore string back to HEX
	
	my $nbtroot_btentrykey = readbin($offset,8,'Q*'); #find BTKey
	my $nbtroot_ref_offset = readbin($offset + 16,8,'Q*'); #find offset
	my $nbt_child_num_entries = readbin($nbtroot_ref_offset + 488,1,'C*');  #find number of entries in referenced page
	my $nbt_child_bid = readbin($offset + 504,8,'Q*'); # get bid of page file
	my $nbt_child_sizeof_btentry = readbin($nbtroot_ref_offset + 490,1,'C*'); # get size of entries
	$depth_root = readbin($nbtroot_ref_offset + 491,1,'C*'); #check the depth
	
	$writer->startTag("details"); #save details in XML
		$writer->startTag("nid"); $writer->characters($nbtroot_btentrykey); $writer->endTag("nid");
		$writer->startTag("bid"); $writer->characters($nbtroot_btentry_bid); $writer->endTag("bid");
		$writer->startTag("block-offset"); $writer->characters($nbtroot_ref_offset); $writer->endTag("block-offset");
		$writer->startTag("num-of-entries"); $writer->characters($nbt_child_num_entries); $writer->endTag("num-of-entries");
		$writer->startTag("internal"); $writer->characters($nbtroot_btentry_bid_i); $writer->endTag("internal");
	$writer->endTag("details");	
	
	#parse through all child elements
	if ($depth_root > 0) {
		nbt_depthover0($nbtroot_ref_offset,$nbt_child_num_entries,$nbt_child_sizeof_btentry,$depth_root);
	}
	else {
		nbt_depth0($nbtroot_ref_offset,$nbt_child_num_entries,$nbt_child_sizeof_btentry,$depth_root);
	}
	
	$offset = $offset + $nbtroot_sizeof_btentry; #go to the next root page
	$writer->endTag("child");
} #end of main for loop

$writer->endTag("nbt");
### END OF NBT PARSING ###

### START OF BBT PARSING ###
my $bbtroot = readbin(240,8,'Q*'); #BBT root page offset
my $bbtroot_bid = readbin($bbtroot + 504,8,'Q*'); #BBT root page BID

#get data from the BBT root page
my $bbtroot_num_entries = readbin($bbtroot + 488,1,'C*'); #get number of entries in page file
my $bbtroot_sizeof_btentry = readbin($bbtroot + 490,1,'C*'); #get size of entries
$depth = readbin($bbtroot + 491,1,'C*'); #check the depth of the page

#save this data in XML
$writer->startTag("bbt");
$writer->startTag("bbt-root-details");
	$writer->startTag("bid"); $writer->characters($bbtroot_bid); $writer->endTag("bid");
	$writer->startTag("block-offset"); $writer->characters($bbtroot); $writer->endTag("block-offset");
	$writer->startTag("num-of-entries"); $writer->characters($bbtroot_num_entries); $writer->endTag("num-of-entries");
$writer->endTag("bbt-root-details");

printf "Parsing through the block BTREE...\n";
printf "Number of entries: $bbtroot_num_entries\n";

#lets start to parse block BTREE
for (my $i=1; $i<=$bbtroot_num_entries; $i++) { #get info for all entries first
	
	$writer->startTag("child", "num" => $i, "depth" => $depth);
	print "Parsing entry $i...";
	
	seek( FH, $bbtroot + 8, 0 ); #get block ID (BID)
	read( FH, $var, 8 );
	my $bbtroot_btentry_bid = unpack('b*', $var); #unpack to binary string
	my $bbtroot_btentry_bid_i = substr($bbtroot_btentry_bid, 1, 1); #check if it is an internal block
	if($bbtroot_btentry_bid_i == 1) { $bbtroot_btentry_bid_i= "yes" } else { $bbtroot_btentry_bid_i = "no" }
	$bbtroot_btentry_bid = substr($bbtroot_btentry_bid, 2, 62); 
	$bbtroot_btentry_bid = unpack('Q*', $var); #restore string back to HEX

	my $bbtroot_btentrykey = readbin($bbtroot,8,'Q*'); #find BTKey
	my $bbtroot_ref_offset = readbin($bbtroot + 16,8,'Q*'); #find offset
	my $bbt_child_num_entries = readbin($bbtroot_ref_offset + 488,1,'C*'); #find number of entries
	my $bbt_child_bid = readbin($bbtroot + 504,8,'Q*'); #get bid of page file
	my $bbt_child_sizeof_btentry = readbin($bbtroot_ref_offset + 490,1,'C*'); #get size of entries
	$depth_root = readbin($bbtroot_ref_offset + 491,1,'C*'); #check the depth

	#save details in XML
	$writer->startTag("details");
		$writer->startTag("nid"); $writer->characters($bbtroot_btentrykey); $writer->endTag("nid");
		$writer->startTag("bid"); $writer->characters($bbtroot_btentry_bid); $writer->endTag("bid");
		$writer->startTag("block-offset"); $writer->characters($bbtroot_ref_offset); $writer->endTag("block-offset");
		$writer->startTag("num-of-entries"); $writer->characters($bbt_child_num_entries); $writer->endTag("num-of-entries");
		$writer->startTag("internal"); $writer->characters($bbtroot_btentry_bid_i); $writer->endTag("internal");
	$writer->endTag("details");	
	
	#parse through all child elements
	if ($depth_root > 0) {
		bbt_depthover0($bbtroot_ref_offset,$bbt_child_num_entries,$bbt_child_sizeof_btentry,$depth_root);
	}
	else {
		bbt_depth0($bbtroot_ref_offset,$bbt_child_num_entries,$bbt_child_sizeof_btentry,$depth_root);
	}

	$bbtroot = $bbtroot + $bbtroot_sizeof_btentry; #go to the next root page
	$writer->endTag("child");
	
}
$writer->endTag("bbt");

printf "Number of Message Objects: $message_count\n";
#printf "Number of Attachment Objects: $attachment_count\n";
printf "Node structure saved to ./$output_filename\n";

#lets finish up here
$writer->endTag("root");
$writer->end();
$output->close();
close(FH);